团队协作中Git Commit应该如何规范化
Published in:2026-02-26 |

@[toc]

一、背景

  • Git每次提交代码都需要写commit message,否则就不允许提交。一般来说,commit message应该清晰明了,说明本次提交的目的,具体做了什么操作……但是在日常开发中,大家的commit message千奇百怪,中英文混合使用、fix bug等各种笼统的message司空见怪,这就导致后续代码维护成本特别大,有时自己都不知道自己的fix修的是什么bug。
  • 而近期,我们团队内有好几个大需求都需要多个研发同学all in一个开发分支,但是在开发过程中我发现很多研发同学提交的commit信息“五花八门”,这就导致在定位问题时不知道对方或者自己的每笔提交是什么含义,无法快速定位问题。
  • 基于以上这些问题,我们希望通过某种方式来约束用户的git commit message,让规范更好的服务于质量,提高大家的研发效率。一旦约束了commit message,意味着我们将慎重的进行每一次提交,不能再一股脑的把各种各样的改动都放在一个git commit里面,这样一来整个代码改动的历史也将更加清晰。

二、一般的commit message格式

2.1 文字版

注:如果不想看长篇文字的,可以直接看下面的流程图

对于一般的commit约束规范为:

  1. 检查提交者的邮箱 (user.email)
    • 它会读取你本地 Git 配置的 user.email。
    • 规则:
      • 邮箱不能为空。
      • 邮箱必须以 @xxx(公司名后缀).com 或 @xxx.net 结尾。
    • 如果不满足,提交会被阻止,并提示你如何通过 git config 命令设置正确的邮箱。
  2. 检查提交者的姓名 (user.name)
    • 它会读取你本地 Git 配置的 user.name。
    • 规则:
      • 姓名不能为空。
    • 如果不满足,提交会被阻止,并建议你设置为中文名。
  3. 检查提交信息的编码
    • 它会读取你写的提交信息文件。
    • 规则:
      • 提交信息必须是 UTF-8 编码。
    • 如果不满足,提交会被阻止。
  4. 检查提交信息的格式 (核心功能)
    • 这是这个hook最重要的检查。
    • 规则: 提交信息的第一行必须以下列前缀之一开头:
      • feat: (用于提交新功能)
      • fix: (用于修复 bug)
      • docs: (用于修改文档)
      • style: (用于代码格式调整,不影响功能)
      • refactor: (用于重构代码)
      • test: (用于增加或修改测试)
      • chore: (用于构建过程或辅助工具的变动)
      • 例外情况: 它也允许以下 Git 自动生成的提交信息开头:
        • Revert (当你执行 git revert 时)
        • Merge branch (当你执行 git merge 时)
        • cherry pick (当你执行 git cherry-pick 时)
    • 如果不满足以上任何一种格式,提交会被阻止,并向你展示所有允许的格式和它们的含义,同时提供一个链接让你查看更详细的规范。

这里展开对具体格式提交格式讲讲

  • 格式: :

    • type(必填):用于说明git commit的类别,只允许使用下面的标识。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    * feat:新功能(feature)
    * fix:修复bug
    * docs:文档更新
    * style:格式更新(修改空白字符,格式缩进,补全缺失的分号等,没有改变代码逻辑)
    * refactor:重构代码(既没有新增功能,也没有修复 bug)
    * perf:性能, 体验优化
    * test:新增测试用例或是更新现有测试
    * revert:回滚到上一个版本。
    * xxxff:代码合并。
    * sync:同步主线或分支的Bug。
    * build:主要目的是修改项目构建系统(例如 glup,webpack,rollup 的配置等)的提交
    • subject(必填):subject是commit目的的简短描述。建议使用中文,结尾不加句号或其他标点符号。
      在这里插入图片描述
  • 示例:

    • fix: 修改内存泄漏问题
    • feat: 用户查询接口开发

2.2 流程图版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  graph TD
A[用户执行 git commit] --> B{钩子开始执行};

B --> C{检查 user.email};
C -- 不合法 --> D[打印邮箱错误信息];
C -- 合法 --> E{检查 user.name};

D --> X([终止提交]);

E -- 不合法 --> F[打印姓名错误信息];
E -- 合法 --> G{检查提交信息的编码};

F --> X;

G -- 非 UTF-8 --> H[打印编码错误信息];
G -- UTF-8 --> I{检查提交信息格式};

H --> X;

I -- 格式正确 --> J([允许提交]);
I -- 格式错误 --> K[打印格式错误信息和规范];

K --> X;
subgraph "格式检查细节"
I1[获取 Commit Message] --> I2{"Message 是否以<br>feat: | fix: | docs: | style: | <br>refactor: | test: | chore: <br>Revert | Merge branch | cherry pick<br>开头?"}
I2 -- 是 --> I;
I2 -- 否 --> I;
end

style X fill:#f9f,stroke:#333,stroke-width:2px;
style J fill:#9f9,stroke:#333,stroke-width:2px;

三、不规范commit示例

在旧规范下依旧存在较多不规范的commit case,其中有些case是可以通过在提交前进行强制拦截的。

case 问题 建议
feat: update data for search 全英文描述 尽量使用中文
eg:更新RN相关的资源
feat: 更新登陆样式&修改进入页面的链路 多处功能改动参杂着bug作为一笔提交 每个 commit 只完善一个功能/修复一个问题,保证提交commit的原子性
fix: ui样式修复 描述过于简单,无法根据commit知晓修改的功能 用简洁的语言描述具体的工作
eg:
fix: 问一问落地页输入框样式修复
feat: DSL升级以支持Kitt引擎
feat: 提高效率
feat: 提高效率
多次重复提交同一条commit 不要在同一个分支下提交重复的commit信息,如果解决的是一类问题,建议squash成一笔提交
feat: gjfahekfhsarjgjkajrgkntkzghndkztnbhknbhkgjdnthkjgnkdnhjk 提交的字符过长 建议subject的字符数不超过72个。
(因为Git 工具通常在 72 字符处自动折行,超过会导致信息被截断,影响阅读。)
fix: 修复了一个线上问题 描述信息subject的首尾存在多余的空格 subject的首部至多有一个空格,尾部不应该含有空格
feat: 你好啊! 末尾有特殊字符,比如?。等 commit信息末尾不应该有特殊字符

四、制定新规范

4.1 强制约束类

  • 格式: <type>:[最多一个空格]<subject>[无特殊字符,无空格]
    在原有规范的基础上,新增规范如下:

    注⚠️:如果确实需要紧急合入,则可用--force命令,即 git commit -m "feat: 这是一次紧急提交,忽略新增规范的约束 --force"

在这里插入图片描述

新规范对应的流程图如下:

图太大了,请将手机/电脑旋转90度“食用”

在这里插入图片描述

4.2 建议提倡类

  • 原子性提交:每个 commit 只完善一个功能/修复一个问题
  • 功能完整性:每个 commit 都应该是可编译、可运行的
  • 逻辑分离:重构、优化和功能开发分开提交
  • 语义明确:commit描述要清晰、简洁明了,尽量使用中文

五、hook脚本安装

  1. 创建hook脚本

    已亲测无问题,对应的是第四章节的规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#!/usr/bin/ruby
#
# An example hook script to check the commit log message.
# Called by "git commit" with one argument, the name of the file
# that has the commit message. The hook should exit with non-zero
# status after issuing an appropriate message if it wants to stop the
# commit. The hook is allowed to edit the commit message file.
#
# To enable this hook, rename this file to "commit-msg".

# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

# This example catches duplicate Signed-off-by lines.

# test "" = "$(grep '^Signed-off-by: ' "$1" |
# sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || {
# echo >&2 Duplicate Signed-off-by lines.
# exit 1
# }

# "feat: 新功能提交"
# "fix: 修补bug"
# "docs: 文档修改"
# "style: 代码风格,不影响代码运行的变动"
# "refactor: 重构,即非新功能也非bug"
# "test: 测试用例修改"
# "chore: 构建过程或辅助工具的变动"

committer_email = `git config user.email`.force_encoding('UTF-8').strip
if (committer_email.nil?) or (committer_email.empty?)
puts "The email check failed, ensure that the Git \"user.email\" is not nil or not empty"
puts "The settings is \"git config --local user.email 'xxx@替换成公司后缀.com'.\" "
exit(1)
elsif !(committer_email.end_with? '@替换成公司后缀.com') && !(committer_email.end_with? '@替换成公司后缀.net')
puts "The email check failed, please use the email with the end of '@替换成公司后缀.[com|net]'"
puts "The settings is \"git config --local user.email 'xxx@替换成公司后缀.[com|net]'.\" "
exit(1)
end

committer_name = `git config user.name`.force_encoding('UTF-8').strip
if (committer_name.nil?) or (committer_name.empty?)
puts "The name check failed, ensure that the Git \"user.name\" is not nil or not empty"
puts "The settings is \"git config --global user.name 'chinese_name' (Suggestion).\" "
exit(1)
end

commit_msg = File.read(ARGV.first)
commit_msg.force_encoding('UTF-8')
if (commit_msg.valid_encoding? == false)
puts "Encoding is not utf-8"
exit(1)
end

if (commit_msg.start_with?("feat:") || commit_msg.start_with?("fix:") || commit_msg.start_with?("docs:") ||
commit_msg.start_with?("style:") || commit_msg.start_with?("refactor:") || commit_msg.start_with?("test:") ||
commit_msg.start_with?("chore:") || commit_msg.start_with?("Revert") || commit_msg.start_with?("Merge branch") || commit_msg.start_with?("cherry pick"))

else
puts "Commit message 格式错误"
puts "参考格式: https://xxx.md"
puts "feat: 新功能提交"
puts "fix: 修补bug"
puts "docs: 文档修改"
puts "style: 代码风格,不影响代码运行的变动"
puts "refactor: 重构,即非新功能也非bug"
puts "test: 测试用例修改"
puts "chore: 构建过程或辅助工具的变动"
exit(1)
end

####################### 新规范 start #######################

### 1. 自动修正首尾多余的空格,并给出警告
full_first_line = commit_msg.split("\n").first
original_full_first_line = full_first_line.dup

# 分割 type 和 subject
parts = full_first_line.split(':', 2)
type = parts[0]
subject = parts[1] || "" # 处理没有 subject 的情况

original_subject = subject.dup

# 修正 subject 的尾部空格
subject.gsub!(/\s+$/, '')

# 修正 subject 的首部空格(允许至多一个空格)
if subject.match?(/^\s{2,}/) # 检查是否存在超过1个的前导空格
subject.gsub!(/^\s+/, ' ') # 将多个前导空格替换为一个空格
end

# 如果 subject 发生了变化,则重组并更新
if subject != original_subject
new_first_line = "#{type}:#{subject}" # 直接拼接,不额外添加空格
puts "[警告⚠️] 检测到提交信息主题部分首尾有多余空格,已自动修正。"

# 重组整个 commit message
lines = commit_msg.split("\n")
lines[0] = new_first_line
commit_msg = lines.join("\n")
File.write(ARGV.first, commit_msg)
end

### 2. 基础规范检查完成,现在处理 --force 和新规范

# 检查提交信息内部是否包含 --force
is_force_commit = commit_msg.strip.end_with?('--force')
clean_commit_msg = is_force_commit ? commit_msg.gsub(/--force\s*$/, '').strip : commit_msg

# 如果是强制提交,将清理后的 message 写回文件
if is_force_commit
File.write(ARGV.first, clean_commit_msg)
end

# 获取最终的(经过空格修正后的)提交信息进行后续检查
final_commit_msg = File.read(ARGV.first).force_encoding('UTF-8')
final_first_line = final_commit_msg.split("\n").first.strip

# 检查 subject 是否为空(这个检查始终执行,即使是 --force)
final_parts = final_first_line.split(':', 2)
final_subject = (final_parts[1] || "").strip
if final_subject.empty?
if is_force_commit
puts "[警告⚠️] 提交信息的主题部分为空。【已强制提交message】"
else
puts "[错误❌] 提交信息的主题部分不能为空。"
puts "请提供有意义的提交描述,例如: feat: 添加用户语音搜索功能"
exit(1)
end
end


if is_force_commit
puts "[警告⚠️] 检测到 --force 标志,已跳过部分提交规范检查。"
# 检查第一行 commit信息的末尾不应该有。.??!!
if final_first_line.end_with?(".") || final_first_line.end_with?("?") || final_first_line.end_with?("!") || final_first_line.end_with?("。") || final_first_line.end_with?("?") || final_first_line.end_with?("!")
puts "[警告⚠️] 提交信息的第一行末尾不应包含标点符号 (. ? ! 。 ? !)。【已强制提交message】"
end

# 检查第一行 commit信息最大不超过72个字符
if final_first_line.length > 72
puts "[警告⚠️] 提交信息的第一行长度建议不超过 72 个字符 (当前: #{final_first_line.length})。【已强制提交message】"
end

# 在同一个git分支下,禁止提交信息与历史重复
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
historic_commits = `git log #{current_branch} --pretty=format:%s`.split("\n")
if historic_commits.include?(final_first_line)
puts "[警告⚠️] 当前分支 \"#{current_branch}\" 已存在相同的提交信息。【已强制提交message】"
end

else
# 严格执行新规范检查
### 3. 检查第一行 commit信息的末尾不应该有。.??!!
if final_first_line.end_with?(".") || final_first_line.end_with?("?") || final_first_line.end_with?("!") || final_first_line.end_with?("。") || final_first_line.end_with?("?") || final_first_line.end_with?("!")
puts "[错误❌] 提交信息的第一行末尾不应包含标点符号 (. ? ! 。 ? !)"
exit(1)
end

### 4. 检查第一行 commit信息最大不超过72个字符
if final_first_line.length > 72
puts "[错误❌] 提交信息的第一行长度不能超过 72 个字符。"
puts "当前长度: #{final_first_line.length}"
exit(1)
end

### 5. 在同一个git分支下,禁止提交信息与历史重复
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
historic_commits = `git log #{current_branch} --pretty=format:%s`.split("\n")
if historic_commits.include?(final_first_line)
puts "[错误❌] 当前分支 \"#{current_branch}\" 已存在相同的提交信息。"

puts "请修改提交信息以确保其独特性。"
exit(1)
end

end

####################### 新规范 end #######################

# echo "commit-msg: "
# echo $1
# exit 1
  1. 修改将 buildsystem/githooks 目录视为存放钩子脚本的地方
    1
    git config core.hooksPath buildsystem/githooks
  2. 赋予执行权限
    1
    chmod +x buildsystem/githooks/commit-msg
  3. 将文件权限的变更添加到暂存区
    1
    git add buildsystem/githooks/commit-msg
  4. 进行代码commit

六、推荐commit插件

Next:
匿名对象被弱引用后提前回收的问题